/*///////////////////////////////////////////////////////////
*
* FILE:		server.c
* AUTHOR:	Paul Spangler and Tuan Dang
* PROJECT:	CS 3251 Project 2 - Professor Traynor
* DESCRIPTION:	Network Server Code
*
*////////////////////////////////////////////////////////////

/*Included libraries*/

#include <stdio.h>	  /* for printf() and fprintf() */
#include <sys/socket.h>	  /* for socket(), connect(), send(), and recv() */
#include <arpa/inet.h>	  /* for sockaddr_in and inet_addr() */
#include <stdlib.h>	  /* supports all sorts of functionality */
#include <unistd.h>	  /* for close() */
#include <string.h>	  /* support any string ops */
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <dirent.h>
#include <errno.h>

#include "arraylist.h"
#include "linkedlist.h"
#include "common.h"

#define MAX_NAME 255
#define MAX_PENDING 5
#define DATA_DIR "data"
#define CLIENT_TIMOUT 120

/* Struct passed as arguments to new threads. */

typedef struct ThreadArgs
{
	int clientSocket;
} ThreadArgs;

/* Struct representing the data in the arraylist. */

typedef struct UserData
{
	char *name;
	ll_list_t history;
} UserData;

/* Globals */

arraylist_t *users;
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;

/* Compares a UserData struct with a string. */

int CompareUser(void *user, void *name)
{
	return strcmp(((UserData *)user)->name, (char *)name) == 0;
}

/**
 * Processes the friends message.
 * Returns 0 to close the connection.
 */

int ProcessFriends(ReadData *rd, FILE *clientStream, char *nameBuffer)
{
	int endOfMessage = 0;

	do
	{
		int x;
		int nameLen = read_string(rd, nameBuffer, MAX_NAME + 1);

		if (nameLen < 0)
		{
			perror("Could not read message data, closing connection");
			return 0;
		}

		if (nameLen >= MAX_NAME + 1)
		{
			fputs("Truncating user name to 255 bytes\n", stderr);
			nameLen = MAX_NAME;
		}

		endOfMessage = (nameBuffer[nameLen] == '\n');

		if (nameLen > 0)
		{
			nameBuffer[nameLen] = '\0';

			if ((errno = pthread_rwlock_rdlock(&lock)) != 0)
			{
				perror("Could not obtain read lock, closing connection");
				return 0;
			}

			if (arraylist_search(users, nameBuffer, &x, &CompareUser))
			{
				UserData *user = arraylist_get(users, x);
				Coordinate *coord = ll_get_back(&user->history);

				printf("Friends: user %s found at location %f, %f.\n", nameBuffer, coord->lattitude, coord->longitude);

				fputc(1, clientStream);
				fwrite(coord, sizeof(Coordinate), 1, clientStream);
			}
			else
			{
				Coordinate coord;

				printf("Friends: user %s not found.\n", nameBuffer);

				fputc(0, clientStream);
				fwrite(&coord, sizeof(coord), 1, clientStream);
			}

			pthread_rwlock_unlock(&lock);

			fputs(nameBuffer, clientStream);
			fputc('\t', clientStream);
		}
	}
	while (!endOfMessage);

	fputc('\n', clientStream);
	fflush(clientStream);
	return -1;
}

/**
 * Processes the update message.
 * Returns 0 to close the connection.
 */

int ProcessUpdate(ReadData *rd, FILE *clientStream, char *nameBuffer)
{
	int nameLen;
	Coordinate *coord = malloc(sizeof(Coordinate));

	if (coord == NULL)
	{
		perror("Could not create user data, closing connection");
		return 0;
	}

	if (!read_data(rd, coord, sizeof(Coordinate)))
	{
		perror("Could not read message data, closing connection");
		free(coord);
		return 0;
	}

	nameLen = read_string(rd, nameBuffer, MAX_NAME + 1);

	if (nameLen < 0)
	{
		perror("Could not read message data, closing connection");
		free(coord);
		return 0;
	}

	if (nameLen == 0 || nameLen >= MAX_NAME + 1)
	{
		puts("Update: name not given or had to be truncated.\n");

		fputc(0, clientStream);
	}
	else
	{
		FILE *userFile;

		nameBuffer[nameLen] = '\0';

		if ((errno = pthread_rwlock_wrlock(&lock)) != 0)
		{
			perror("Could not obtain write lock, closing connection");
			free(coord);
			return 0;
		}

		printf("Update: user %s is now at location %f, %f.\n", nameBuffer, coord->lattitude, coord->longitude);

		userFile = fopen(nameBuffer, "a");

		if (userFile == NULL)
		{
			perror("Could not open user file, sending bad user name");
			free(coord);
			fputc(0, clientStream);
		}
		else
		{
			int x;

			if (!arraylist_search(users, nameBuffer, &x, &CompareUser))
			{
				/* User does not exist, add them. */

				UserData *user = malloc(sizeof(UserData));

				if (user == NULL || (user->name = malloc(sizeof(char) * nameLen)) == NULL)
				{
					perror("Could not create user data, closing connection");
					fclose(userFile);
					pthread_rwlock_unlock(&lock);
					free(coord);

					if (user != NULL)
					{
						free(user);
					}

					return 0;
				}

				strcpy(user->name, nameBuffer);
				memset(&user->history, 0, sizeof(user->history));

				x = users->count;

				if (!arraylist_add(users, user))
				{
					perror("Could not create user data, closing connection");
					fclose(userFile);
					pthread_rwlock_unlock(&lock);
					free(user->name);
					free(user);
					free(coord);
					return 0;
				}
			}

			if (!ll_add_back(&((UserData *)arraylist_get(users, x))->history, coord))
			{
				perror("Could not create user data, closing connection");
				fclose(userFile);
				pthread_rwlock_unlock(&lock);
				free(coord);
				return 0;
			}

			if (fwrite(coord, sizeof(Coordinate), 1, userFile) != 1)
			{
				UserData *user = arraylist_get(users, x);

				perror("Could not write new coordinate, closing connection");
				fclose(userFile);
				pthread_rwlock_unlock(&lock);

				free(ll_remove_back(&user->history)); /* free(coord) */

				if (user->history.count == 0)
				{
					arraylist_remove(users, x);
					free(user->name);
					free(user);
				}

				return 0;
			}

			fclose(userFile);
			fputc(1, clientStream);
		}

		pthread_rwlock_unlock(&lock);
	}

	fputc('\n', clientStream);
	fflush(clientStream);

	return -1;
}

/**
 * Processes the history message.
 * Returns 0 to close the connection.
 */

int ProcessHistory(ReadData *rd, FILE *clientStream, char *nameBuffer)
{
	int nameLen = read_string(rd, nameBuffer, MAX_NAME + 1);

	if (nameLen < 0)
	{
		perror("Could not read message data, closing connection");
		return 0;
	}

	if (nameLen == 0 || nameLen >= MAX_NAME + 1)
	{
		unsigned int count = 0;

		puts("History: name not given or had to be truncated.\n");

		if (fwrite(&count, sizeof(count), 1, clientStream) != 1)
		{
			perror("Could not write no results, closing connection");
			return 0;			
		}
	}
	else
	{
		int x;

		nameBuffer[nameLen] = '\0';

		if ((errno = pthread_rwlock_rdlock(&lock)) != 0)
		{
			perror("Could not obtain read lock, closing connection");
			return 0;
		}

		if (!arraylist_search(users, nameBuffer, &x, &CompareUser))
		{
			unsigned int count = 0;

			printf("History: user %s does not exist.\n", nameBuffer);

			if (fwrite(&count, sizeof(count), 1, clientStream) != 1)
			{
				perror("Could not write no results, closing connection");
				pthread_rwlock_unlock(&lock);
				return 0;			
			}
		}
		else
		{
			ll_node_t *node;
			UserData *user = arraylist_get(users, x);

			printf("History: retrieving %d results for user %s.\n", user->history.count, nameBuffer);

			if (fwrite(&user->history.count, sizeof(user->history.count), 1, clientStream) != 1)
			{
				perror("Could not write number of results, closing connection");
				pthread_rwlock_unlock(&lock);
				return 0;			
			}

			node = user->history.head;

			while (node != NULL)
			{
				if (fwrite(node->data, sizeof(Coordinate), 1, clientStream) != 1)
				{
					perror("Could not write result, closing connection");
					pthread_rwlock_unlock(&lock);
					return 0;			
				}

				node = node->next;
			}
		}

		pthread_rwlock_unlock(&lock);
	}

	fputc('\n', clientStream);
	fflush(clientStream);

	return -1;
}

/**
 * Loads a user's data from file.
 * Returns nonzero on success.
 */

static int LoadUser(char *name)
{
	FILE *userFile;
	UserData *data = malloc(sizeof(UserData));

	if (data == NULL)
	{
		perror("Cannot load data");
		return 0;
	}

	data->name = malloc(sizeof(char) * strlen(name));

	if (data == NULL)
	{
		perror("Cannot load data");
		return 0;
	}

	strcpy(data->name, name);
	userFile = fopen(data->name, "r");
	memset(&data->history, 0, sizeof(data->history));

	if (userFile == NULL)
	{
		fprintf(stderr, "Cannot open user %s file: ", data->name);
		perror(NULL);
	}
	else
	{
		size_t read;
		Coordinate coord;

		while ((read = fread(&coord, sizeof(coord), 1, userFile)) == 1)
		{
			Coordinate *coordPtr = malloc(sizeof(Coordinate));

			if (coordPtr == NULL)
			{
				perror("Cannot load data");
				fclose(userFile);
				return 0;
			}

			*coordPtr = coord;

			if (!ll_add_back(&data->history, coordPtr))
			{
				perror("Cannot load data");
				fclose(userFile);
				return 0;
			}
		}

		fclose(userFile);
	}

	if (data->history.count == 0)
	{
		free(data->name);
		free(data);
	}
	else if (!arraylist_add(users, data))
	{
		perror("Cannot load data");
		return 0;
	}

	return -1;
}

/**
 * Loads existing history data for users.
 */

static void LoadData()
{
	struct dirent *dp;
	DIR *dataDir = opendir(DATA_DIR);

	if (dataDir == NULL && errno == ENOENT &&
	    mkdir(DATA_DIR, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) == 0)
	{
		dataDir = opendir(DATA_DIR);
	}

	if (dataDir == NULL)
	{
		perror("Cannot create or open the data directory");
		exit(EXIT_FAILURE);
	}

	if (chdir(DATA_DIR) != 0)
	{
		perror("Cannot change into the data directory");
		closedir(dataDir);
		exit(EXIT_FAILURE);
	}

	puts("Loading existing data...");

	while ((dp = readdir(dataDir)) != NULL)
	{
		UserData *data;

		if (dp->d_name[0] == '.')
		{
			continue;
		}

		if (!LoadUser(dp->d_name))
		{
			closedir(dataDir);
			exit(EXIT_FAILURE);
		}
	}

	closedir(dataDir);

	printf("Loaded data for %d user%c\n", users->count, users->count == 1 ? ' ' : 's');
}

/**
 * Starts the server listening on serverPort.
 * Returns the created socket.
 */

static int StartServer(in_port_t serverPort)
{
	struct timeval tv;
	struct sockaddr_in serverAddress;
	int serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (serverSocket < 0)
	{
		perror("Failed to open socket");
		exit(EXIT_FAILURE);
	}

	memset(&tv, 0, sizeof(tv));
	tv.tv_sec = CLIENT_TIMOUT;

	if (setsockopt(serverSocket, SOL_SOCKET, SO_RCVTIMEO, (struct timeval *)&tv, sizeof(tv)) < 0)
	{
		perror("Failed to set socket timeout");
		exit(EXIT_FAILURE);
	}

	memset(&serverAddress, 0, sizeof(serverAddress));
	serverAddress.sin_family = AF_INET;
	serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddress.sin_port = htons(serverPort);

	if (bind(serverSocket, (struct sockaddr *)&serverAddress, sizeof(serverAddress)) < 0)
	{
		perror("Failed to bind socket");
		exit(EXIT_FAILURE);
	}

	if (listen(serverSocket, MAX_PENDING) < 0)
	{
		perror("Failed to listen on socket");
		exit(EXIT_FAILURE);
	}

	printf("Server is listening on port %d\n", serverPort);

	return serverSocket;
}

/* Entry-point for client threads. */

void *ThreadMain(void *threadArgs)
{
	int readMessages = 1;
	char nameBuffer[MAX_NAME + 1];
	int clientSocket = ((ThreadArgs *)threadArgs)->clientSocket;
	FILE *clientStream = fdopen(clientSocket, "w");
	ReadData rd = read_init(clientSocket);

	free(threadArgs);

	/* Process messages until the client leaves. */

	while (readMessages)
	{
		char indicator;

		/* Get the message indicator (1 byte). */

		if (!read_byte(&rd, &indicator))
		{
			perror("Could not read new message, closing connection");
			break;
		}

		switch (indicator)
		{
			case MESSAGE_UPDATE:
				readMessages = ProcessUpdate(&rd, clientStream, nameBuffer);
				break;

			case MESSAGE_FRIENDS:
				readMessages = ProcessFriends(&rd, clientStream, nameBuffer);
				break;

			case MESSAGE_HISTORY:
				readMessages = ProcessHistory(&rd, clientStream, nameBuffer);
				break;

			case MESSAGE_LEAVE:
				readMessages = 0;
				break;

			default:
				fprintf(stderr, "Unknown message indicator %d, closing connection\n", indicator);
				readMessages = 0;
				break;
		}
	}
	
	fclose(clientStream);
	close(clientSocket);

	printf("Client disconnected from thread %lu\n", (unsigned long)pthread_self());

	return NULL;
}

/**
 * Accepts a connection, starting a new thread to handle the connection.
 */

static void AcceptConnection(int serverSocket)
{
	int clientSocket;
	int clientLength;
	pthread_t thread;
	ThreadArgs *threadArgs;
	pthread_attr_t threadAttr;
	struct sockaddr_in clientAddress;
	socklen_t clientAddressLength = sizeof(clientAddress);

	clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddress, &clientAddressLength);

	if (clientSocket < 0)
	{
		if (errno != EAGAIN && errno != EWOULDBLOCK)
		{
			perror("Unable to accept new client");
		}

		return;
	}

	threadArgs = malloc(sizeof(ThreadArgs));

	if (threadArgs == NULL)
	{
		fprintf(stderr, "Unable to allocate ThreadArgs for new client.\n");
		close(clientSocket);
		return;
	}

	threadArgs->clientSocket = clientSocket;

	/* Create the new thread for the client. */

	if ((errno = pthread_attr_init(&threadAttr)) != 0 ||
	    (errno = pthread_attr_setdetachstate(&threadAttr, PTHREAD_CREATE_DETACHED)) != 0 ||
	    (errno = pthread_create(&thread, NULL, ThreadMain, threadArgs)) != 0)
	{
		perror("Unable to create client thread");
		close(clientSocket);
		free(threadArgs);
		return;
	}

	printf("Client connected to thread %lu\n", (unsigned long)thread);
}

/* Application entry-point. */

int main(int argc, char *argv[])
{
	int serverSocket;

	/* Get the port from the command-line. */

	if (argc != 2)
	{
		fprintf(stderr, "Usage: %s port\n", argv[0]);
		exit(EXIT_FAILURE);
	}

	users = arraylist_create(4);

	if (users == NULL)
	{
		perror("Cannot create user arraylist");
		exit(EXIT_FAILURE);
	}

	LoadData();

	serverSocket = StartServer(atoi(argv[1]));

	/* Accept connections forever. */

	for (;;)
	{
		AcceptConnection(serverSocket);
	}

	/* Should never get here. */

	return EXIT_FAILURE;
}
